#
# Copyright (c) 2023 supercell
#
# SPDX-License-Identifier: BSD-3-Clause
#
require "../text_parser"

module Luce
  class ListItem
    getter lines : Array(Line)
    getter task_list_item_state : TaskListState?

    def initialize(@lines : Array(Line), @task_list_item_state : TaskListState? = nil)
    end
  end

  enum TaskListState
    CHECKED
    UNCHECKED
  end

  # Base class for both ordered and unorderd lists.
  abstract class ListSyntax < BlockSyntax
    def can_parse?(parser : BlockParser) : Bool
      pattern.matches?(parser.current.content) &&
        !Luce.hr_pattern.matches?(parser.current.content)
    end

    private TASK_LIST_CLASS = "task-list-item"

    def can_end_block?(parser : BlockParser) : Bool
      # An empty list cannot interrupt a paragraph. See
      # https://spec.commonmark.org/0.30/#example-285.
      # Ideally, `BlockSyntax.can_end_block?` should be changed to a
      # method which accepts a `BlockParser`, but this would be a
      # breaking change, so we're going with this temporarily.
      # ameba:disable Lint/NotNil
      match = pattern.match(parser.current.content).not_nil!
      # Allow only lists starting with 1 to interrupt paragraphs, if it is an
      # ordered list. See https://spec.commonmark.org/0.30/#example-304.
      # But there should be an exception for nested ordered lists, for example:
      # ```
      # 1.one
      # 2.two
      # 3.three
      # 4.four
      # 5.five
      # ```
      if !parser.parent_syntax.is_a?(ListSyntax) &&
         match[1]? != nil &&
         match[1] != "1"
        return false
      end

      # An empty list cannot interrupt a paragraph. See
      # https://spec.commonmark.org/0.30/#example-285
      (match[2]? != nil && !match[2].empty?) || false
    end

    @@_blocks_in_list : Array(Regex) = [
      Luce.blockquote_pattern,
      Luce.header_pattern,
      Luce.hr_pattern,
      Luce.indent_pattern,
      Luce.list_pattern,
    ] of Regex

    def self.blocks_in_list : Array(Regex)
      @@_blocks_in_list
    end

    def parse(parser : BlockParser) : Node
      match = pattern.match(parser.current.content)
      # ameba:disable Lint/NotNil
      ordered = match.not_nil![1]? != nil
      task_list_parser_enabled = self.class == UnorderedListWithCheckboxSyntax ||
                                 self.class == OrderedListWithCheckboxSyntax
      items = [] of ListItem
      child_lines = [] of Line
      task_list_item_state : TaskListState? = nil

      end_item = ->{
        if !child_lines.empty?
          items << ListItem.new(child_lines, task_list_item_state: task_list_item_state)
          child_lines = [] of Line
        end
      }

      parse_task_list_item = ->(text : String) {
        _pattern = Regex.new(%q{^ {0,3}\[([ xX])\][ \t]})

        if task_list_parser_enabled && _pattern.matches?(text)
          match = _pattern.match(text)
          # ameba:disable Lint/NotNil
          task_list_item_state = match.not_nil![1] == " " ? TaskListState::UNCHECKED : TaskListState::CHECKED
          text.sub(_pattern, "")
        else
          task_list_item_state = nil
          text
        end
      }

      possible_match : Regex::MatchData? = nil
      try_match = ->(_pattern : Regex) {
        possible_match = _pattern.match(parser.current.content)
        possible_match != nil
      }

      list_marker : String? = nil
      indent : Int32? = nil
      # In case the first number in an ordered list is not 1,
      # use it as the "start"
      start_number : Int32? = nil

      blank_lines : Int32? = nil

      while !parser.done?
        current_indent = Luce.string_indentation(parser.current.content) + (parser.current.tab_remaining || 0)
        if parser.current.is_blank_line?
          child_lines << parser.current

          blank_lines += 1 unless blank_lines.nil?
        elsif !indent.nil? && indent <= current_indent
          # A list item can begin with at most one blank line. See:
          # https://spec.commonmark.org/0.30/#example-280
          break if !blank_lines.nil? && blank_lines > 1

          indented_line = Luce.dedent_string(parser.current.content, indent)

          child_lines << Line.new(
            blank_lines.nil? ? indented_line.text : parse_task_list_item.call(indented_line.text),
            tab_remaining: indented_line.tab_remaining
          )
        elsif try_match.call(Luce.hr_pattern)
          # Horizontal rule takes precedence to a new list item.
          break
        elsif try_match.call(Luce.list_pattern)
          blank_lines = nil
          # ameba:disable Lint/NotNil
          match = possible_match.not_nil!
          text_parser = TextParser.new(parser.current.content)
          preceding_whitespaces = text_parser.move_through_whitespace
          marker_start = text_parser.pos
          # ameba:disable Lint/NotNil
          digits = match.not_nil![1]? || ""
          unless digits.empty?
            start_number ||= digits.to_i32
            text_parser.advance_by(digits.size)
          end
          text_parser.advance

          # See https://spec.commonmark.org/0.30/#ordered-list-marker
          marker = text_parser[marker_start...text_parser.pos]

          is_blank = true
          content_whitespaces = 0
          contains_tab = false
          content_block_start : Int32? = nil

          unless text_parser.done?
            contains_tab = text_parser.char_at == Charcode::TAB
            # Skip the first whitespace
            text_parser.advance
            content_block_start = text_parser.pos
            unless text_parser.done?
              content_whitespaces = text_parser.move_through_whitespace

              is_blank = false unless text_parser.done?
            end
          end

          # Changing the bullet or ordered list delimiter starts a new list
          break if !list_marker.nil? && list_marker[-1] != marker[-1]

          # Ends the current list item and starts a new one.
          end_item.call

          # Starts a new list item, the last item will be ending up outside of the
          # `while` loop.
          list_marker = marker
          preceding_whitespaces += digits.size + 2
          if is_blank
            # See http://spec.commonmark.org/0.30/#example-278
            blank_lines = 1
            indent = preceding_whitespaces
          elsif content_whitespaces >= 4
            # See https://spec.commonmark.org/0.30/#example-270.
            #
            # If the list item starts with indented code, we need to _not_ count
            # any indentation past the required whitespace character.
            indent = preceding_whitespaces
          else
            indent = preceding_whitespaces + content_whitespaces
          end

          task_list_item_state = nil
          content = (content_block_start != nil && !is_blank) ? parse_task_list_item.call(text_parser[content_block_start..]) : ""

          content = "  " + content if content.empty? && contains_tab

          child_lines << Line.new(content, tab_remaining: contains_tab ? 2 : nil)
        elsif BlockSyntax.at_block_end?(parser)
          # Done with list
          break
        else
          # If the previous item is a blank line, this means we're done with the
          # list and are starting a new top-level paragraph.
          if !child_lines.empty? && child_lines.last.is_blank_line?
            parser.encountered_blank_line = true
            break
          end

          # Anything else is paragraph continuation text.
          child_lines << parser.current
        end
        parser.advance
      end

      end_item.call
      item_nodes = [] of Node

      items.each { |item| remove_leading_empty_line(item) }
      any_empty_lines = remove_trailing_empty_lines(items)
      any_empty_lines_between_blocks = false
      contains_task_list = false

      items.each do |item|
        checkbox_to_insert : Element?
        unless item.task_list_item_state.nil?
          contains_task_list = true
          checkbox_to_insert = Element.with_tag("input")
          checkbox_to_insert.attributes["type"] = "checkbox"
          if item.task_list_item_state == TaskListState::CHECKED
            checkbox_to_insert.attributes["checked"] = "true"
          end
        end

        item_parser = BlockParser.new(item.lines, parser.document)
        children = item_parser.parse_lines(parent_syntax: self)
        item_element = if checkbox_to_insert.nil?
                         Element.new("li", children)
                       else
                         e = Element.new("li", add_checkbox(children, checkbox_to_insert))
                         e.attributes["class"] = TASK_LIST_CLASS
                         e
                       end
        item_nodes << item_element
        any_empty_lines_between_blocks =
          any_empty_lines_between_blocks || item_parser.encountered_blank_line?
      end

      # Must strip paragraph tags if the list is "tight"
      # https://spec.commonmark.org/0.30/#lists
      list_is_tight = !any_empty_lines && !any_empty_lines_between_blocks

      if list_is_tight
        # We must post-process the list items, converting any top-level paragraph
        # elements to just text elements.
        item_nodes.each do |item|
          if item.is_a? Element
            is_task_list = item.attributes["class"]? == TASK_LIST_CLASS
            children = item.children
            unless children.nil?
              last_node : Node? = nil
              i = 0
              while i < children.size
                child = children[i]
                if child.is_a? Element && child.tag == "p"
                  # ameba:disable Lint/NotNil
                  child_content = child.children.not_nil!
                  if last_node.is_a? Element && !is_task_list
                    child_content.insert(0, Text.new("\n"))
                  end

                  children.delete_at(i)
                  # ameba:disable Lint/NotNil
                  Luce.array_insert_all(children, i, child.children.not_nil!)
                end

                last_node = child
                i += 1
              end
            end
          end
        end
      end

      list_element = Element.new(ordered ? "ol" : "ul", item_nodes)
      if ordered && start_number != 1
        list_element.attributes["start"] = "#{start_number}"
      end

      list_element.attributes["class"] = "contains-task-list" if contains_task_list
      list_element
    end

    private def add_checkbox(children : Array(Node), checkbox : Element) : Array(Node)
      unless children.empty?
        first_child = children.first
        if first_child.is_a?(Element) && first_child.tag == "p"
          first_child.children.not_nil!.insert(0, checkbox)
          return children
        end
      end

      [checkbox] + children
    end

    protected def remove_leading_empty_line(item : ListItem) : Nil
      if !item.lines.empty? && item.lines.first.is_blank_line?
        item.lines.delete_at(0)
      end
    end

    # Removes any trailing
    protected def remove_trailing_empty_lines(items : Array(ListItem)) : Bool
      any_empty = false
      i = 0
      while i < items.size
        if items[i].lines.size == 1
          i += 1
          next
        end
        while !items[i].lines.empty? && items[i].lines.last.is_blank_line?
          if i < items.size - 1
            any_empty = true
          end
          items[i].lines.pop
        end
        i += 1
      end
      any_empty
    end
  end
end
